#!/usr/bin/env python
#Copyright (c) 2011 Isaac Dawson (WBTS Project)
#Permission is hereby granted, free of charge, to any person obtaining a copy 
#of this software and associated documentation files (the "Software"), to deal 
#in the Software without restriction, including without limitation the rights 
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
#copies of the Software, and to permit persons to whom the Software is furnished
#to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
#THE SOFTWARE.
import os
import glob
from binascii import b2a_base64
import uuid
import datetime

from twisted.web import resource, server, static, xmlrpc
from zope.interface import Interface, implements
from twisted.python import components, log
from twisted.internet import defer

from wbts.utils import web_utils


class TestCases(xmlrpc.XMLRPC):

    def __init__(self, service, path, mapped_dir):
        xmlrpc.XMLRPC.__init__(self)
        self.mMgmtService = service
        self.mPath = path+os.sep
        self.mMappedDir = mapped_dir
    
    def _removeNonCases(self, path, all_files, excluded_dirs=[]):
        """
        _removeNonCases - Takes the path, all the files (in a list) and
        an excluded_dir list. The caller will need to check the excluded_dirs
        to see if any were added. We will create the full path in the event
        a directory is found. To signify you'd like to excluded a directory
        and all of that directories sub directories, just append a / to the
        end of the directory name like: test/ or test\. We then create the
        full path of which directory (and it's sub directories) will
        be excluded. As for removing files, it's just 1 file name per line.
        TODO: Maybe implement a regex to match files on and remove as
        necessary?
        @params
        path - The path to where the exclude.txt file is.
        all_files - A list of files to exclude from testing
        excluded_dirs - A list to put in directories we want excluded from
        testing
        @returns
        A list of test cases, one file per element.
        """
        all_files.remove('exclude.txt')
        cases = all_files
        try:
            exclusion_list = open(path+os.sep+'exclude.txt')
            
            for non_case in exclusion_list.readlines():
                non_case = non_case.replace('\r\n', '')
                non_case = non_case.replace('\n', '')
                # kinda lame but to exclude a path we just append
                # a / or \ to the end of the name to signify
                # it's a directory. (like exclude_this/ or exclude_this\)
                if (non_case.endswith('/' or non_case.endswith('\\'))):
                    # [:-1] just removes the slash
                    excluded_dirs.append(path+os.sep+non_case[:-1])
                    continue
                # make sure it's not empty, and just call the list's
                # remove method to pop it out.
                if (non_case != ''):
                    try:
                        cases.remove(non_case)
                    except ValueError:
                        log.err("ERROR %s not found in %s"%(non_case, path))
        except IOError, msg:
            log.err("Could not open exclude.txt from %s"%path)
        return cases
    
    def _checkTypeHasFiles(self, test_type):
        """
        _checkTypeHasFiles - A simple method to validate we even have files
        for testing. 
        """
        if ('..' in test_type):
            log.err("ERROR We have a wise guy sending '..' in the path")
            return False

        for path,sub_dir, files in os.walk(self.mPath+os.sep+test_type):
            if (files != []):
                # If we have files, but it's just the exclude.txt or
                # all files are excluded, then we need to just continue
                if ('exclude.txt' in files):
                    cases = self._removeNonCases(path, files)
                    # if we get nothing back we need to check the next dir.
                    if (cases == []):
                        continue
                return True
        return False
    

    def xmlrpc_getTestsOfType(self, test_type):
        # for sanity, probably unnecessary.
        test_type = test_type.replace('\\', os.sep)
        test_type = test_type.replace('//', os.sep)
        
        cases = []
        case_list = []
        excluded_dirs = [".svn"] #temporarily exclude .svn
        excluded = False
        t_type = test_type
        # assume all is the top level directory.
        if (test_type == 'all'):
            excluded_dirs.append("automation") # no need to test automation here
            t_type = ''
            
        # Uh make sure we even have files heh.
        ret = self._checkTypeHasFiles(t_type)
        if (ret == False):
            log.msg("Requested type [%s] has no files."%test_type)
            return []
        for path, sub_dir, files in os.walk(self.mPath+os.sep+t_type):
            # check to see if the path we are in is considered excluded
            # via the top level's exclusion list.
            for excluded_dir in excluded_dirs:
                # the thinking is, if our path contains our exclusion
                # directory path, then we don't need it. This has the added
                # affect of excluding ALL sub-directories from that path!
                if (path.find(excluded_dir[:-1]) != -1):
                    excluded = True
                    break

            # Don't bother with this path at all and move on.
            if (excluded ==  True):
                # reset our flag
                excluded = False
                continue

            # see if we have an exclude file in the current path
            if ('exclude.txt' in files):
                # remove all the cases / dirs
                cases = self._removeNonCases(path, files, excluded_dirs)
            else:
                # no exclude, just set our cases as the files.
                cases = files

            # we only need to return the uri to the testcase here.
            relative_path = self.mMappedDir + path[len(self.mPath):]
            relative_path = relative_path.replace('\\','/')
            # loop over each case we have and add them to the list.
            for case in cases:
                case_list.append('/' + relative_path + '/' + case)
        return case_list
        
    def xmlrpc_getTestCount(self, test_type='all'):
        return len(self.xmlrpc_getTestsOfType(test_type))
    
    def xmlrpc_getTestTypes(self):
        files = os.listdir(self.mPath)
        if (".svn" in files):
            files.remove(".svn")
        return files
        
    def _cb_Err(self, msg):
        log.err("ERROR An error occurred: %s"%msg)
        return msg
    
    def _cb_Response(self, data):
        # 1 is we have 1 result. 0 means none.
        if (data == None or (len(data) == 0) ):
            return 0
        else:
            return 1
        
    def xmlrpc_getTestProgress(self, client_id, testcase):
        db = self.mMgmtService.mDbConn["test_results"]
        d = db.find_one({'client_id':client_id,'testcase':testcase})
        d.addCallback(self._cb_Response)
        d.addErrback(self._cb_Err)
        return d
    
    def _cb_Ok(self, data):
        return 0
    
    def xmlrpc_failTest(self, client_id, testcase):
        db = self.mMgmtService.mDbConn["test_results"]
        pre_db = self.mMgmtService.mDbConn["pretest_results"]
        d = pre_db.find_one({'client_id': client_id, 'testcase':testcase})
        d.addCallback(web_utils.fail_test, self, pre_db, db)
        d.addErrback(self._cb_Err)
        return d
    
class TestCaseLogger(resource.Resource):
    implements(resource.IResource)
    isLeaf = False
    def __init__(self, service):
        resource.Resource.__init__(self)
        self.mMgmtService = service
        self.putChild('savePreTest', SavePreTest(self.mMgmtService))
        self.putChild('saveTest', SaveTest(self.mMgmtService))
        self.putChild('failTest', FailTest(self.mMgmtService))
    
 

class TestSaver(resource.Resource):
    implements(resource.IResource)
    def __init__(self, service):
        resource.Resource.__init__(self)
        self.mMgmtService = service
        self.type = "test_results"
        #self.db = self.mMgmtService.mDbConn["test_results"]
        #self.pre_db = self.mMgmtService.mDbConn["pretest_results"]
    
    def _cb_Err(self, err, request):
        log.err("ERROR An error occurred saving %s!"%self.type)
        log.err(err)
        return web_utils.json_err_response(request, "error saving results")

    def _cb_Ok(self, data, request):
        return web_utils.json_ok_response(request)
    
    def _preTestExists(self, client_id, testcase, request):
        d = self.mMgmtService.mDbConn["pretest_results"].find_one(
            {'client_id':client_id, 'testcase':testcase}
        )
        d.addErrback(log.err)
        d.addCallback(web_utils.remove_temp_record, self,
                      self.mMgmtService.mDbConn["pretest_results"], False,
                      request)
        d.addErrback(log.err)
        return d
        
    def render_GET(self, request):
        request.setHeader("content-type", "application/json")
        if (request.args == {}):
            request = web_utils.custom_arg_parser(request)

        testcase = target_host = result = expected_result = test_passed = ''
        user_agent = host = description = output = client_id = testcase_url = ''
        input = ''
        
        if (web_utils.not_empty(request.args, "testcase_url")):
            testcase_url = request.args["testcase_url"][0]
            client_id = web_utils.find_client_id(testcase_url)
            try:
                testcase = testcase_url[
                    testcase_url.rfind('/')+1:testcase_url.find('?')
                    ]
            except ValueError, msg:
                log.err("ERROR Parsing testcase from %s failed"%testcase_url)
        
        # no need to do this if we are in pretest_results        
        if (self.type == "test_results"):
            # copy the pretest data into our results table if it does
            self._preTestExists(client_id, testcase, request)
        
        if (web_utils.not_empty(request.args, "target_host")):
            target_host = request.args["target_host"][0]
            
        if (request.getHeader('host') != None):
            host = request.getHeader('host')
        
        if (request.getHeader('user-agent') != None):
            user_agent = request.getHeader('user-agent')
            
        if (web_utils.not_empty(request.args, "result")):
            result = request.args["result"][0]
        
        if (web_utils.not_empty(request.args, "expected_result")):
            expected_result = request.args["expected_result"][0]
        
        if (web_utils.not_empty(request.args, "test_passed")):
            test_passed = request.args["test_passed"][0]
        
        if (web_utils.not_empty(request.args, "description")):
            description = request.args["description"][0]
        
        if (web_utils.not_empty(request.args, "input")):
            input = request.args["input"][0]
        
        if (web_utils.not_empty(request.args, "output")):
            output = request.args["output"][0]
        
        result_doc = self.prepResultDocument(description, user_agent, testcase,
                                             host, target_host, result,
                                             expected_result, input, output,
                                             testcase_url, client_id,
                                             test_passed)
        
        # save it to the proper store.
        if (self.type == "test_results"):
            db = self.mMgmtService.mDbConn["test_results"]
        else:
            db = self.mMgmtService.mDbConn["pretest_results"]
            
        d = db.insert(result_doc)
        d.addCallback(self._cb_Ok, request)
        d.addErrback(self._cb_Err, request)
        return server.NOT_DONE_YET
        
    render_POST = render_GET
    
    
class SaveTest(TestSaver):
    implements(resource.IResource) 
    def __init__(self, service):
        resource.Resource.__init__(self)
        TestSaver.__init__(self, service)
        self.type = "test_results"
        
                
    def prepResultDocument(self, description, user_agent, testcase, host,
                           target_host, result, expected_result, input, output,
                           testcase_url, client_id, test_passed):
        
        result_doc = {'time':str(datetime.datetime.utcnow()),
                      'description': description,
                      'user_agent': user_agent,
                      'testcase':testcase,
                      'host':b2a_base64(host),
                      'target_host':b2a_base64(target_host),
                      'result':b2a_base64(result),
                      'expected_result':b2a_base64(expected_result),
                      'input':b2a_base64(input),
                      'output':b2a_base64(output),
                      'testcase_url':testcase_url,
                      'client_id':client_id,
                      'test_passed': test_passed
                      }
        return result_doc
    


class SavePreTest(TestSaver):
    implements(resource.IResource)
    def __init__(self, service):
        resource.Resource.__init__(self)
        TestSaver.__init__(self, service)
        self.type = "pretest_results"
        
        
    def prepResultDocument(self, description, user_agent, testcase, host,
                           target_host, result, expected_result, input, output,
                           testcase_url, client_id, test_passed):
    
        result_doc = {'time':str(datetime.datetime.utcnow()),
                      'description': description,
                      'user_agent': user_agent,
                      'testcase':testcase,
                      'host':b2a_base64(host),
                      'target_host':b2a_base64(target_host),
                      'result':b2a_base64('TEST FAILED'),
                      'expected_result':b2a_base64(expected_result),
                      'input': b2a_base64(input),
                      'output':b2a_base64('TEST FAILED'),
                      'testcase_url':testcase_url,
                      'client_id':client_id,
                      'test_passed': 'false'
                      }
    
        return result_doc
    
        
# This isn't really used, but available if you want to mess with it.
# Takes 1 parameter, testcase_url (which should contain the client_id as a
# query argument)
class FailTest(resource.Resource):
    implements(resource.IResource)
    def __init__(self, service):
        resource.Resource.__init__(self)
        self.mMgmtService = service
    
    def _cb_Err(self, err, request):
        log.err("An error occurred moving the failed test over")
        log.err(err)
        return web_utils.json_err_response(request, "error saving results")
    
    def _cb_Ok(self, something, request):
        return web_utils.json_ok_response(request)
        
    def render_GET(self, request):
        request.setHeader("content-type", "application/json")
        if (request.args == {}):
            request = web_utils.custom_arg_parser(request)
        db = self.mMgmtService.mDbConn["test_results"]
        pre_db = self.mMgmtService.mDbConn["pretest_results"]
        testcase_url = client_id = testcase = ''
                 
        if (web_utils.not_empty(request.args, "testcase_url")):
            testcase_url = request.args["testcase_url"][0]
            client_id = web_utils.find_client_id(testcase_url)
            try:
                testcase = testcase_url[
                    testcase_url.rfind('/')+1:testcase_url.find('?')
                    ]
            except ValueError, msg:
                log.err("ERROR Parsing testcase from %s failed"%testcase_url)
                self._cb_Err("Unable to find testcase or client id", request)
        
        result_doc = {'client_id':client_id, 'testcase':testcase}
        
        # find the document from our pretest results db
        d = pre_db.find_one(result_doc)
        # web_utils.fail_test takes the result. and copies it into the real db
        d.addCallback(web_utils.fail_test, self, pre_db, db, request)
        d.addErrback(self._cb_Err, request)
        return server.NOT_DONE_YET
        
    render_POST = render_GET

class RequestLogger(resource.Resource):
    implements(resource.IResource)
    isLeaf = False
    def __init__(self, service):
        resource.Resource.__init__(self)
        self.mMgmtService = service
    
